import Lang from "../Lang";
export default class Tokenizer {
    private source: string;
    private index: number;
    public line: number;
    private stack: Array<string>;
    private readingString: boolean;
    public stringEndsWith: string;
    constructor(proto: string) {
        this.source = "" + proto;
        this.index = 0;
        this.line = 1;
        this.stack = [];
        this.readingString = false;
        this.stringEndsWith = Lang.STRINGCLOSE;
    }



    /**
     * Gets the next token and advances by one.
     * @return {?string} Token or `null` on EOF
     * @throws {Error} If it's not a valid proto file
     * @expose
     */
    public next(): string|null {
        if (this.stack.length > 0) {
            return <string>this.stack.shift();
        }
        if (this.index >= this.source.length) {
            return null; // No more tokens
        }
        if (this.readingString) {
            this.readingString = false;
            return this._readString();
        }
        var repeat, last;
        do {
            repeat = false;
            // Strip white spaces
            while (Lang.WHITESPACE.test(last = this.source.charAt(this.index))) {
                this.index++;
                if (last === "\n") this.line++;
                if (this.index === this.source.length) return null;
            }
            // Strip comments
            if (this.source.charAt(this.index) === '/') {
                if (this.source.charAt(++this.index) === '/') { // Single line
                    while (this.source.charAt(this.index) !== "\n") {
                        this.index++;
                        if (this.index == this.source.length) return null;
                    }
                    this.index++;
                    this.line++;
                    repeat = true;
                } else if (this.source.charAt(this.index) === '*') { /* Block */
                    last = '';
                    while (last + (last = this.source.charAt(this.index)) !== '*/') {
                        this.index++;
                        if (last === "\n") this.line++;
                        if (this.index === this.source.length) return null;
                    }
                    this.index++;
                    repeat = true;
                } else {
                    throw (new Error("Invalid comment at line " + this.line + ": /" + this.source.charAt(this.index) + " ('/' or '*' expected)"));
                }
            }
        } while (repeat);
        if (this.index === this.source.length) return null;

        // Read the next token
        var end = this.index;
        Lang.DELIM.lastIndex = 0;
        var delim = Lang.DELIM.test(this.source.charAt(end));
        if (!delim) {
            end++;
            while (end < this.source.length && !Lang.DELIM.test(this.source.charAt(end))) {
                end++;
            }
        } else {
            end++;
        }
        var token = this.source.substring(this.index, this.index = end);
        if (token === Lang.STRINGOPEN) {
            this.readingString = true;
            this.stringEndsWith = Lang.STRINGCLOSE;
        } else if (token === Lang.STRINGOPEN_SQ) {
            this.readingString = true;
            this.stringEndsWith = Lang.STRINGCLOSE_SQ;
        }
        return token;
    };



    /**
 * Reads a string beginning at the current index.
 */
    private _readString(): string {
        Lang.STRING.lastIndex = this.index - 1; // Include the open quote
        let match;
        if ((match = Lang.STRING.exec(this.source)) !== null) {
            let s = match[1];
            this.index = Lang.STRING.lastIndex;
            this.stack.push(this.stringEndsWith);
            return s;
        }
        throw (new Error("Illegal string value at line " + this.line + ", index " + this.index));
    };




    /**
     * Peeks for the next token.
     * @return {?string} Token or `null` on EOF
     * @throws {Error} If it's not a valid proto file
     * @expose
     */
    public peek(): string | null {
        if (this.stack.length === 0) {
            var token = <string>this.next();
            if (token === null) return null;
            this.stack.push(token);
        }
        return this.stack[0];
    };

    /**
     * Returns a string representation of this object.
     * @return {string} String representation as of "Tokenizer(index/length)"
     * @expose
     */
    public toString(): string {
        return "Tokenizer(" + this.index + "/" + this.source.length + " at line " + this.line + ")";
    }
}